package org.maplanning;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map.Entry;

/**
 * @author Jaak Simm
 */
public class GridMap {
	int width;
	int height;
	
	String mapFile=null;

	ArrayList<ArrayList<Integer>> map;
	ArrayList<int[]> goalStates;

	int[] agent;
	HashMap<Character, int[]> actionMap;

	public final static int WALL='#';
	public final static int START='S';
	public final static int GOAL='G';

	/**
	 * constructs empty GridMap
	 */
	public GridMap() {
		this.height = 0;
		this.width  = 0;
		this.map = new ArrayList<ArrayList<Integer>>();
		this.goalStates = new ArrayList<int[]>();
		this.actionMap = new HashMap<Character, int[]>();
		actionMap.put('U', new int[]{-1, 0} );
		actionMap.put('D', new int[]{ 1, 0} );
		actionMap.put('L', new int[]{ 0,-1} );
		actionMap.put('R', new int[]{ 0, 1} );
	}

	public int get(int[] x) {
		return get(x[0], x[1]);
	}
	
	public boolean isValidCoordinate(int[] x) {
		return x[0]<height && x[1]<width && x[0]>=0 && x[1]>=0;
	}

	/**
	 * Return map tile at coordinate (row, col)
	 * @param row
	 * @param col
	 * @return
	 */
	public int get(int row, int col) {
		if (row>=height || col>=width || row<0 || col<0) {
			throw new IllegalArgumentException("Row or col is too big or negative");
		}
		return map.get(row).get(col);
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		for (int row=0; row<height; row++) {
			for (int col=0; col<width; col++) {
				sb.append((char)get(row,col));
			}
			sb.append('\n');
		}
		return sb.toString();
	}

	/**
	 * Creates GridMap object from text map file.
	 * @param file
	 * @return
	 */
	public static GridMap loadMap(String file) {
		GridMap grid = new GridMap();
		grid.mapFile = file;
		try {
			// opening file:
			BufferedReader br = new BufferedReader(new InputStreamReader(
					new DataInputStream(new FileInputStream(file))));
			String strLine;
			// reading data:
			while ((strLine = br.readLine()) != null)   {
				if (grid.height==0) {
					grid.width = strLine.length();
				}
				if (strLine.length()==0) {
					continue; // skipping empty lines
				}
				if (grid.width!=strLine.length()) {
					// wrong number of characters in a line
					throw new RuntimeException( String.format("File '%s' does not have all lines of the same length.", file));
				}
				grid.height++;
				// separating the row by character a time:
				ArrayList<Integer> row = new ArrayList<Integer>();
				for (int i=0; i<grid.width; i++) {
					row.add( (int) strLine.charAt(i) );
					// if found goal, adding it to goalStates:
					if (row.get(i)==GOAL) {
						grid.goalStates.add(new int[]{ grid.height-1, i });
					}
				}
				grid.map.add(row);
			}
			// Close the input stream
			br.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
			throw new RuntimeException();
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException();
		}

		return grid;
	}

	/**
	 * Searches for first 'S' in GridMap and sets agent there
	 */
	public void initializeAgent() {
		for (int row=0; row<height; row++) {
			for (int col=0; col<width; col++) {
				if (get(row, col)==START) {
					// found starting square:
					agent = new int[]{row, col};
					return;
				}
			}
		}
		throw new RuntimeException("There is no start square (S) on the map.");
	}

	/**
	 * Executes move by the agent.
	 * @param action
	 */
	public void move(char action) {
		if (!this.actionMap.containsKey(action)) {
			// not a valid action:
			throw new IllegalArgumentException("Action "+(char)action+" not valid.");
		}
		int[] newSq = this.actionMap.get(action).clone();
		newSq[0] += this.agent[0];
		newSq[1] += this.agent[1];

		if (newSq[0]<0 || newSq[0]>=this.height) {
			return; // no movement at the edge
		}
		if (newSq[1]<0 || newSq[1]>=this.width) {
			return; // no movement at the edge
		}
		// no move if wall '#':
		if (get(newSq)==WALL) {
			return;
		}
		this.agent[0] = newSq[0];
		this.agent[1] = newSq[1];
	}

	/**
	 * Executes sequence of actions
	 * @param actions
	 */
	public void move(char... actions) {
		for (int i=0; i<actions.length; i++) {
			move(actions[i]);
		}
	}

	/**
	 * @param dir
	 * @return action that would make change dir
	 * (throws IllegalArgumentException if no action)
	 */
	public char getAction(int[] dir) {
		for (Entry<Character, int[]> e : this.actionMap.entrySet()) {
			if ( Arrays.equals(dir, e.getValue()) ) {
				return e.getKey();
			}
		}
		throw new IllegalArgumentException("No action that gives "+Arrays.toString(dir)+".");
	}

	/**
	 * @param x
	 * @return returns all neighbors to x (non-walls and inside the map)
	 */
	public ArrayList<int[]> getNeighbors(int[] x) {
		ArrayList<int[]> neighbors = new ArrayList<int[]>();
		for (int[] d : this.actionMap.values()) {
			int[] neighbor = x.clone();
			neighbor[0] += d[0];
			neighbor[1] += d[1];
			if (neighbor[0]<0 || neighbor[0]>=this.height) {
				continue;
			}
			if (neighbor[1]<0 || neighbor[1]>=this.width) {
				continue;
			}
			if (get(neighbor)==WALL) {
				continue;
			}
			neighbors.add(neighbor);
		}
		return neighbors;
	}

	/**
	 * Returns L1 distance between x1 and x2
	 */
	public static double distanceL1(int[] x1, int[] x2) {
		return Math.abs(x1[0]-x2[0]) + Math.abs(x1[1]-x2[1]);
	}

	/**
	 * Initializes agent and executes for maxSteps or until agent reaches goal.
	 * @param strat
	 * @param maxSteps
	 * @return
	 */
	public RunResults executeStrategy(StrategyI strat, int maxSteps) {
		return executeStrategy(strat, maxSteps, null);
	}

	/**
	 * Executes for maxSteps or until agent reaches goal.
	 * Initializes agent if initAgent==true.
	 * @return path and actions
	 */
	public RunResults executeStrategy(StrategyI strat, int maxSteps, int[] initAgent) {
		RunResults rr = new RunResults(this);
		// initializing and adding the first state to the path
		if (initAgent==null) {
			this.initializeAgent();
		} else {
			this.agent = initAgent;
		}
		rr.path.add(this.agent.clone());
		// action-move loop:
		for (int n=0; n<maxSteps; n++) {
			char action = strat.act(this);
			this.move(action);

			// storing results:
			rr.path.add(this.agent.clone());
			rr.actions.add(action);

			// finishing run if reached goal:
			if (get(agent)==GOAL) {
				rr.reachedGoal = true;
				break;
			}
		}
		return rr;
	}

	public interface StrategyI {
		public char act(GridMap grid);
	}

	/**
	 * Object for holding results of a run in GridMap
	 * returned by executeStrategy()
	 * @author Jaak Simm
	 */
	public class RunResults {
		ArrayList<int[]> path;
		ArrayList<Character> actions; // n-th action is taken from n-th state in path
		boolean reachedGoal;
		GridMap map;

		public RunResults(GridMap map) {
			this.map  = map;
			this.path = new ArrayList<int[]>();
			this.actions = new ArrayList<Character>();
			this.reachedGoal = false;
		}
	}
	
	/**
	 * Outputs HTML file that displays the map and the path of the agent. 
	 * @param file
	 * @param run - can be null
	 */
	public void outputHtml5(String file, RunResults run) {
		Writer output;
		try {
			output = new BufferedWriter(new FileWriter(file));
			writeContents("html-source/results-top.html", output);
			
			// outputting data into data object:
			output.write("var data={};\n");
			output.write(String.format("data.height = %d;\n", this.height) );
			output.write(String.format("data.width = %d;\n",  this.width) );
			output.write(String.format("data.map = '%s';\n",  this.mapFile) );
			
			// outputting walls:
			output.write("data.walls=[\n");
			for (int row=0; row<this.height; row++) {
				for (int col=0; col<this.width; col++) {
					if (this.get(row,col)==WALL) {
						output.write(String.format("[%d,%d],", row, col) );
					}
				}
			}
			output.write("\n];\n");
			
			// goals:
			output.write("data.goals=[\n");
			for (int[] goal : this.goalStates) {
				output.write(String.format("[%d,%d],", goal[0], goal[1]) );
			}
			output.write("\n];\n");
			
			// agent path:
			output.write("data.path=[\n");
			if (run!=null) {
				for (int[] x : run.path) {
					output.write(String.format("[%d,%d],", x[0], x[1]) );
				}
			}
			output.write("\n];\n");
			
			writeContents("html-source/results-bottom.html", output);
			output.close();
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException();
		}
	}
	
	/**
	 * Writes contents of {@code file} into {@code output}.  
	 * @param file
	 * @param output
	 * @throws IOException
	 */
	static void writeContents(String file, Writer output) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(
				new DataInputStream(new FileInputStream(file))));
		String strLine;
		// reading data:
		while ((strLine = br.readLine()) != null)   {
			output.write(strLine);
			output.write("\n");
		}
		br.close();
	}
}
